require 'ostruct'

module Gamifier
  class Model < OpenStruct
    attr_writer :engine

    # The engine to use when creating or updating records. Default to
    # the global Gamifier engine if not set via <tt>#engine=</tt>
    # method.
    def engine
      @engine ||= Gamifier.engine
    end

    def attributes; @table.dup; end

    def save(options = {})
      if new?
        options.has_key?(:async) ? async_create : create
      else
        update
      end
    end

    # Allow to update only the specific attributes given in the +opts+ Hash.
    def update_attributes(opts = {})
      raise Error, "Can't update an object if it has not been saved yet" if new?
      update(opts)
    end

    def payload_for_submission(opts = {})
      attr_to_keep = opts.empty? ? attributes : opts

      attr_to_keep = attr_to_keep.reject{|k,v|
        self.class.special_attributes.include?(k.to_sym) || (
          !self.class.mutable_attributes.empty? && !self.class.mutable_attributes.include?(k.to_sym)
        )
      }

      # Nested hashes are dumped as JSON payload.
      attr_to_keep.each do |k,v|
        attr_to_keep[k] = encode(k.to_sym,v) || case v
        when Hash
          MultiJson.dump(v)
        # Lazyloaded attributes
        when Proc
          v.call
        else
          v.to_s
        end
      end

      h = { self.class.container => attr_to_keep }

      self.class.special_attributes.each do |key|
        h[key] = send(key)
      end

      h
    end

    # Overwrite in descendants to specifically encode a key/value pair.
    def encode(key, value)
      return nil
    end

    def _id
      super || attributes[:id]
    end

    def destroy
      return true if new?
      engine.transmit :delete, path
    end

    def replace_if_successful(response)
      if response.kind_of?(Hash)
        response.each{|k,v| send("#{k}=".to_sym, v)}
        self
      else
        response
      end
    end

    # Returns true if the current object does not exist on the server.
    def new?
      self._id.nil?
    end

    def path
      [self.class.path, self._id || "undefined"].join("/")
    end

    # To be included  as instance methods into the Collection class used
    # to instantiate and find objects. Add more in descendants if you
    # want to add more methods. See Gamifier::Player for an example.
    module FinderMethods
      # Fetch all the records according to the given query parameters.
      # If a block is given, it will automatically lazy-load all the
      # pages and yield each entry, until all the items have been
      # loaded, or until the user returns from the block.
      def all(params = {}, &block)
        params[:page] ||= 1
        params[:per_page] ||= 50
        res = engine.transmit(:get, path, :query => params)
        if res.kind_of?(Hash)
          entries = map_to_models(res)
          if block
            # Lazy load all the pages
            entries.each{|entry| yield(entry)}
            params[:page] += 1
            all(params, &block) unless res['paging'].empty? || res['data'].empty?
          end
          entries
        else
          res
        end
      end

      def find_by(key, value, params = {})
        all(params) do |entry|
          return entry if entry.respond_to?(key) && entry.send(key) == value.to_s
        end
        return nil
      end

      def find_by_name(name, params = {})
        find_by(:name, name, params)
      end

      def find_by_label(label, params = {})
        find_by(:label, label, params)
      end

      def find(key, params = {})
        res = engine.transmit(:get, [path, key].join("/"), :query => params)
        select_first_entry_if_any(res)
      end

      def find_or_create(key, params = {})
        find(key, params) || build(params).save
      end

      protected
      # Selects the first entry out of a data array, or out of a single
      # data entry.
      def select_first_entry_if_any(response)
        if response.kind_of?(Hash) && response.has_key?('data')
          entry = [response['data']].flatten[0]
          return nil if entry.nil?
          build(entry)
        else
          response
        end
      end

      def map_to_models(response)
        response['data'].map{|h| build(h)}
      end
    end

    module ClassMethods
      def reset!
        @path = nil
      end

      # Sets or retrieve the path to use to access the resource on the
      # remote server. By default the path will be the pluralized
      # container name (naive pluralization: container+s).
      def path(value = nil)
        if value.nil?
          @path ||= container+"s"
        else
          @path = value
        end
      end

      # Declare attributes that must be submitted outside the default
      # container.
      def special_attributes *values
        store_or_get("@special_attributes", values)
      end

      # Define the attributes that will be sent when creating or
      # updating a model.
      def mutable_attributes *values
        store_or_get("@mutable_attributes", values)
      end

      def store_or_get(variable, values)
        if values.empty?
          v = instance_variable_get(variable)
          v ||= []
        else
          instance_variable_set(variable, values.map{|v| v.to_sym})
        end
      end
      # Returns the container to use when submitting data. E.g. when
      # submitting user attributes as user[email]=xxx, the container is
      # 'user'. Basically, this is the underscored class name.
      def container
        Gamifier.underscore(name)
      end
    end

    extend ClassMethods

    protected

    def create
      res = engine.transmit :post, self.class.path, :body => payload_for_submission
      replace_if_successful(res)
    end

    def async_create
      res = engine.transmit :post, 'activities/async_create', :body => payload_for_submission
      replace_if_successful(res)
    end

    def update(opts = {})
      res = engine.transmit :put, path, :body => payload_for_submission(opts)
      replace_if_successful(res)
    end
  end

end
